/*------------------------------------------------------------------------------
* GeoVISTA Center, Penn State Geography Deptartment*
* Copyright (c), 2002, GeoVISTA Center
* All Rights Researved.

* Original Authors: Frank Hardisty, Bonan Li
* $Author: jmacgill $
*
* $Date: 2004/12/03 19:26:34 $
*
* $Id: MapCanvasExtension.java,v 1.6 2004/12/03 19:26:34 jmacgill Exp $
*
* To Do:
*
 ------------------------------------------------------------------------------*/

package edu.psu.geovista.app.map;

import edu.psu.geovista.app.coordinator.CoordinationManager;
import edu.psu.geovista.data.condition.ConditionManager;
import edu.psu.geovista.data.geog.DataSetForApps;
import edu.psu.geovista.data.transform.ShapeAffineTransform;
import edu.psu.geovista.data.transform.ShapeTransformer;
import edu.psu.geovista.symbolization.AffineTransformModifier;
import edu.psu.geovista.symbolization.BivariateColorSymbolClassification;
import edu.psu.geovista.symbolization.BivariateColorSymbolClassificationSimple;
import edu.psu.geovista.symbollibrary.SymbolInterface;
import edu.psu.geovista.ui.ExcentricLabelClient;
import edu.psu.geovista.ui.ExcentricLabels;
import edu.psu.geovista.ui.Fisheyes;
import edu.psu.geovista.ui.cursor.GeoCursors;
import edu.psu.geovista.ui.event.*;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This class handles the rendering of layer-independent objects like
 * tooltips and the image used for buffering, and manages the layers,
 * which render themselves to the image.
 *
 * This class also transforms spatial data into user space.
 *
 * This class is intended to be used inside other components like the
 * GeoMap or the PlotMatrix.
 */
public class MapCanvasExtension
    extends JPanel
    implements ComponentListener,
    ActionListener, MouseListener,
    MouseMotionListener,
    SelectionListener,
    IndicationListener,
    DataSetListener,
    SpatialExtentListener,
    ExcentricLabelClient{
  public static int MODE_SELECT = 0; //default mode
  public static int MODE_ZOOM_IN = 1;
  public static int MODE_ZOOM_OUT = 2;
  public static int MODE_PAN = 3;
  public static int MODE_EXCENTRIC = 4;
  public static int MODE_FISHEYE = 5;
  public static int MODE_MAGNIFYING = 6;

  private static final int CLASSIFIER_TYPE_BIVARIATE = 0;
  private static final int CLASSIFIER_TYPE_UNIVARIATE = 1;

  private transient Vector spatialDataTypes = new Vector();
  private transient Vector shapeLayers;
  //Data for each layer
  private transient DataSetForApps[] dataSets;

  private transient ShapeTransformer transformer;

  private transient int mouseX1;
  private transient int mouseX2;
  private transient int mouseY1;
  private transient int mouseY2;

  private transient int indication = Integer.MIN_VALUE;
  private transient int activeLayer;
  private transient int[] classification;
  private transient int[] conditionArray;
  private transient int[] selectedObservations;
  private transient Color[] objectColors;
  private transient int currColorColumnX = -1;
  private transient int currColorColumnY = -1;

  //Some variables
  private transient Rectangle2D savedSrc = null;
  private transient Image drawingBuff;
  private transient Image[] drawingBuff2;
  private transient AffineTransform panningXForm = new AffineTransform();
  private transient Rectangle2D spatialDataFullExtent;
  private transient int mode;
  protected transient ExcentricLabels exLabels;
  private transient String tipText = "";
  private transient Font tipFont;

  //Colors
  private transient double[] dataColorX;
  private transient double[] dataColorY;
  private transient Color colorSelection = Color.yellow;
  private transient Color colorIndication = Color.blue;


  //Some handy tools
  protected transient BivariateColorSymbolClassificationSimple
      bivarColorClasser =
      new BivariateColorSymbolClassificationSimple();
  private transient GeoCursors cursors = new GeoCursors();
  private transient float[] dash = new float[] {5f, 7f, 5f};
  private transient BasicStroke dashStroke = new BasicStroke( (float) 2.0,
      BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_ROUND,
      (float) 10.0, dash, 0);
  private transient BasicStroke solidStroke = new BasicStroke(2f);
  protected Fisheyes fisheyes;

  //Some controls
  protected transient boolean autofit = false;
  private transient boolean fitOnce = false;
  private transient boolean drawTip = true;
  private transient boolean drawBox = false;
  private transient boolean selOriginalColorMode = true;

  /**
   * Constructor
   */
  public MapCanvasExtension() {
    super();

    this.setPreferredSize(new Dimension(300, 300));
    this.addComponentListener(this);
    this.addMouseListener(this);
    this.addMouseMotionListener(this);
    int greyAmt = 170;
    Color bgColor = new Color(greyAmt, greyAmt, greyAmt);
    this.setBackground(bgColor);

    this.shapeLayers = new Vector(10, 10);
    this.transformer = new ShapeAffineTransform();
    this.mouseX1 = -2;
    this.drawTip = true;
    this.mode = this.MODE_SELECT;
  }

  private boolean symbolset = false;
  /***
   * Set symbols, which have been configured according to data values.
   */
  public void setSymbols(SymbolInterface[] sbs) {
    LayerInterface ls = (LayerInterface)this.shapeLayers.elementAt(this.activeLayer);
    ls.setSymbols(sbs);
    this.symbolset = true;
    this.paintDrawingBuff();//may help with redraw issues?
    this.repaint();
  }

  /***
   * Set colors for observations. Normally this method is called by other components
   * that classify or cluster data observations and assign colors.
   */
  public void setObservationColors(Color[] obsColors) {
    if (this.shapeLayers.size() < 1 || this.dataSets[this.activeLayer] == null) {
      return;
    }

    if (this.dataSets[this.activeLayer].getNumObservations() != obsColors.length) {
      System.out.println("###ERROR: obsColors.length != number of observations"+ 
                         this.dataSets[this.activeLayer].getNumObservations()+" != "+obsColors.length);
      return;
    }
    this.objectColors = obsColors;

    LayerShapeExtension ls = (LayerShapeExtension) this.shapeLayers.elementAt(activeLayer);
    ls.setObjectColors(this.objectColors);

    this.paintDrawingBuff();
    this.repaint();
  }

  private void sendSpatialDataToLayer(LayerShapeExtension l, Shape[] spatialData) {
    l.setSpatialData(spatialData);
    l.setParentSize(this.getHeight(), this.getWidth());
  }

  /**
   * Given a region (defined by a rect), transform shapes into that region.
   */
  public Shape[] findUserSpaceSpatialData(Shape[] originalShapes,
                                          Rectangle2D dest, boolean useSavedSrc) {
    Shape[] originalData = originalShapes;
    //two cases for finding src rectangle: second or later set of shapes, or first.
    Rectangle2D src = null;

    if (useSavedSrc && (this.savedSrc != null)) {
      src = this.savedSrc;
    }
    else {
      src = findFullExtentRect(originalShapes);
      this.savedSrc = src;
    }

    AffineTransform xForm = AffineTransformModifier.makeGeogAffineTransform(src,
        dest,
        true,
        true);
    Shape[] returnShapes = this.transformer.makeTransformedShapes(
        originalShapes, xForm);

    return returnShapes;
  }

  /****
   * Tranform data from data space to user space,
   * i.e., change the coordinates to screen coordinates.
   */
  public Shape[] findFullExtentSpatialData(Shape[] originalShapes,
                                           boolean useSavedSrc) {
    Rectangle2D dest = new Rectangle2D.Float(0, 0, this.getWidth(),
                                             this.getHeight());
    Shape[] returnShapes = this.findUserSpaceSpatialData(originalShapes, dest,
        useSavedSrc);

    return returnShapes;
  }


  /**
   * Zoom in/out
   * @param dest
   */
  private void zoomNewExtent(Rectangle2D dest) {
    for (Enumeration e = this.shapeLayers.elements(); e.hasMoreElements(); ) {
      LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
      Shape[] originalShapes = ls.getOriginalSpatialData();
      Shape[] returnShapes = this.findUserSpaceSpatialData(originalShapes, dest,
          ls.getIsAuxiliary());
      this.sendSpatialDataToLayer(ls, returnShapes);
    }

    paintDrawingBuff();
    this.repaint();
  }

  private Rectangle2D findFullExtentRect(Shape[] someShapes) {
    Shape[] someData = someShapes;
    double xMax;
    double xMin;
    double yMax;
    double yMin;
    xMax = Double.MAX_VALUE * -1;
    xMin = Double.MAX_VALUE;
    yMax = Double.MAX_VALUE * -1;
    yMin = Double.MAX_VALUE;

    for (int i = 0; i < someData.length; i++) {
      Rectangle2D bounding = someData[i].getBounds2D();
      if (bounding.getMaxX() > xMax) {
        xMax = bounding.getMaxX();
      }
      if (bounding.getMinX() < xMin) {
        xMin = bounding.getMinX();
      }
      if (bounding.getMaxY() > yMax) {
        yMax = bounding.getMaxY();
      }
      if (bounding.getMinY() < yMin) {
        yMin = bounding.getMinY();
      }
    }
    Rectangle2D src = new Rectangle2D.Double(xMin, yMin, xMax - xMin,
                                             yMax - yMin);
    return src;
  }


  public void zoomFullExtent() {
      LayerShapeExtension ls = (LayerShapeExtension) this.shapeLayers.elementAt(this.activeLayer);
      Shape[] originalShapes = ls.getOriginalSpatialData();
      Shape[] returnShapes = this.findFullExtentSpatialData(originalShapes,
          false);
      this.sendSpatialDataToLayer(ls, returnShapes);

    for (int i=0; i< this.shapeLayers.size(); i++) {
      if (i==this.activeLayer)
        continue;
      ls = (LayerShapeExtension) this.shapeLayers.elementAt(i);
      originalShapes = ls.getOriginalSpatialData();
      returnShapes = this.findFullExtentSpatialData(originalShapes,
          true);
      this.sendSpatialDataToLayer(ls, returnShapes);
    }
    paintDrawingBuff();
    this.repaint();

    LayerShapeExtension activeLayer = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    Rectangle2D rect = this.findFullExtentRect(activeLayer.getSpatialData());
    this.fireSpatialExtentChanged(rect);
  }

  public void spatialExtentChanged(SpatialExtentEvent e) {
    Rectangle2D rect = e.getSpatialExtent();
    this.zoomNewExtent(rect);
  }

  public void zoomIn(int x1, int x2, int y1, int y2) {
    if (x1 > x2) {
      int temp = x1;
      x1 = x2;
      x2 = temp;
    }

    if (y1 > y2) {
      int temp = y1;
      y1 = y2;
      y2 = temp;
    }

    int diffX = x2 - x1;
    int diffY = y2 - y1;
    int boxTolerance = 10;

    if ( (diffX < boxTolerance) && (diffY < boxTolerance)) { //accidental box, should have been a click
      x2 = x1;
      y2 = y1;
    }

    if ( (x1 == x2) && (y1 == y2)) {
      //single click, we want to zoom in a bit with the click as the center
      float scaleFactor = 0.45f;
      int center = x1;
      x1 = center - (int) ( (float)this.getWidth() * scaleFactor);
      x2 = center + (int) ( (float)this.getWidth() * scaleFactor);
      center = y1;
      y1 = center - (int) ( (float)this.getHeight() * scaleFactor);
      y2 = center + (int) ( (float)this.getHeight() * scaleFactor);
    }

    Rectangle2D src = new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1);
    Rectangle2D dest = new Rectangle2D.Float(0, 0, this.getWidth(),
                                             this.getHeight());
    AffineTransform xForm = AffineTransformModifier.makeZoomingAffineTransform(
        src, dest);

    for (Enumeration e = this.shapeLayers.elements(); e.hasMoreElements(); ) {
      LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();

      Shape[] originalShapes = ls.getSpatialData();
System.out.println("layer shape type = " +originalShapes[0].getClass());
      Shape[] returnShapes = this.transformer.makeTransformedShapes(
          originalShapes, xForm);

      this.sendSpatialDataToLayer(ls, returnShapes);
    }

    paintDrawingBuff();
    this.repaint();

    LayerShapeExtension activeLayer = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    Rectangle2D rect = this.findFullExtentRect(activeLayer.getSpatialData());
    this.fireSpatialExtentChanged(rect);
  }

  public void zoomOut(int x1, int x2, int y1, int y2) {
    if (x1 > x2) {
      int temp = x1;
      x1 = x2;
      x2 = temp;
    }

    if (y1 > y2) {
      int temp = y1;
      y1 = y2;
      y2 = temp;
    }

    Rectangle2D src = new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1);
    Rectangle2D dest = new Rectangle2D.Float(0, 0, this.getWidth(),
                                             this.getHeight());
    AffineTransform xForm = AffineTransformModifier.makeZoomingAffineTransform(
        dest, src);

    for (Enumeration e = this.shapeLayers.elements(); e.hasMoreElements(); ) {
      LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();

      Shape[] originalShapes = ls.getSpatialData();
      Shape[] returnShapes = this.transformer.makeTransformedShapes(
          originalShapes, xForm);

      this.sendSpatialDataToLayer(ls, returnShapes);
    }

    paintDrawingBuff();
    this.repaint();

    LayerShapeExtension activeLayer = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    Rectangle2D rect = this.findFullExtentRect(activeLayer.getSpatialData());
    this.fireSpatialExtentChanged(rect);
  }

  public void pan(int x1, int x2, int y1, int y2) {
    int xDiff = x2 - x1;
    int yDiff = y2 - y1;

    Rectangle2D dest = new Rectangle2D.Float(0 + xDiff, 0 + yDiff,
                                             this.getWidth(), this.getHeight());
    Rectangle2D src = new Rectangle2D.Float(0, 0, this.getWidth(),
                                            this.getHeight());
    AffineTransform xForm = AffineTransformModifier.makeGeogAffineTransform(src,
        dest,
        false,
        false);

    for (Enumeration e = this.shapeLayers.elements(); e.hasMoreElements(); ) {
      LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();

      Shape[] originalShapes = ls.getSpatialData();
      Shape[] returnShapes = this.transformer.makeTransformedShapes(
          originalShapes, xForm);

      this.sendSpatialDataToLayer(ls, returnShapes);
    }

    paintDrawingBuff();
    this.repaint();

    LayerShapeExtension activeLayer = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    Rectangle2D rect = this.findFullExtentRect(activeLayer.getSpatialData());
    this.fireSpatialExtentChanged(rect);
  }

  public void panBuff(int x1, int x2, int y1, int y2) {
    int xDiff = x2 - x1;
    int yDiff = y2 - y1;

    //AffineTransform panningXForm = new AffineTransform();
    panningXForm = panningXForm.getTranslateInstance(xDiff, yDiff);
  }

  private void sendColorsToLayers(int numObs) {
    if (this.shapeLayers.size() < 1) {
      return;
    }
    if (this.dataColorX == null) {
      return;
    }
    if (this.dataColorY == null) {
      return;
    }
    if ( (this.objectColors == null) || (this.objectColors.length != numObs)) {
      this.objectColors = new Color[numObs];
    }
    this.objectColors = this.bivarColorClasser.symbolize(this.dataColorX,
        this.dataColorY);

    LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    ls.setObjectColors(this.objectColors);
    paintDrawingBuff();
    this.repaint();
  } //end method

  private void makeToolTip(int arrayIndex) {
    String[] variableNames = dataSets[this.activeLayer].getAttributeNamesNumeric();
    if (arrayIndex < 0 || this.currColorColumnX < 0 || this.currColorColumnY <0) {
      tipText = "";
    }
    else if (this.currColorColumnX == this.currColorColumnY) {
      String xVal = String.valueOf(dataSets[this.activeLayer].getNumericValueAsDouble(
          this.currColorColumnX, arrayIndex));
      String s = "";
      String[] observationNames = this.dataSets[this.activeLayer].getObservationNames();

      if (observationNames != null) {
        s = s + "Name = " + observationNames[arrayIndex] + "\n";
      }

      s = s + variableNames[currColorColumnX - 1] + " = " + xVal;

      this.tipText = s;
    }
    else if ( (this.currColorColumnX > 0) && (this.currColorColumnY > 0)) {
      //setting multi-line tool tip
      //b.setToolTipText("<html>ToolTip : 1st Line<br>2nd Line<br> 3rd Line </html>");
      String xVal = String.valueOf(dataSets[this.activeLayer].getNumericValueAsDouble(
          this.currColorColumnX, arrayIndex));
      String yVal = String.valueOf(dataSets[this.activeLayer].getNumericValueAsDouble(
          this.currColorColumnY, arrayIndex));
      String s = "";
      String[] observationNames = this.dataSets[this.activeLayer].getObservationNames();

      if (observationNames != null) {
        s = s + "Name = " + observationNames[arrayIndex] + "\n";
      }

      s = s + variableNames[currColorColumnX - 1] + " = " + xVal + "\n" +
          variableNames[currColorColumnY - 1] + " = " + yVal;

      this.tipText = s;
    } //if
  }

  protected void tickleColors() {
    //egregious hack
    this.setCurrColorColumnX(this.currColorColumnX);
    this.setCurrColorColumnY(this.currColorColumnY);
  }

  private void autoFitShapes() {
    for (int i = 0; i < this.shapeLayers.size(); i++) {
      LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(i);
      Integer layerTypeInt = (Integer)this.spatialDataTypes.get(i);
      int layerType = layerTypeInt.intValue();

      if (layerType == DataSetForApps.SPATIAL_TYPE_POLYGON) {
        Shape[] originalShapes = ls.getOriginalSpatialData();
        boolean useSavedSrc = ls.getIsAuxiliary();
        Shape[] userShapes = this.findFullExtentSpatialData(originalShapes,
            useSavedSrc);
        this.sendSpatialDataToLayer(ls, userShapes);

        //this.sendColorsToLayers(this.dataSet.getNumObservations());
      }
      else if (layerType == DataSetForApps.SPATIAL_TYPE_POINT) {
        Shape[] originalShapes = ls.getOriginalSpatialData();
        boolean useSavedSrc = ls.getIsAuxiliary();
        Shape[] userShapes = this.findFullExtentSpatialData(originalShapes,
            useSavedSrc);
        LayerPointExtension lp = new LayerPointExtension();
        Point2D[] points = lp.shapesToPoints(userShapes);
        userShapes = lp.findShapesForPoints(points);

        //userSpaceSpatialData.add(userShapes);
        this.sendSpatialDataToLayer(ls, userShapes);
        this.sendColorsToLayers(this.dataSets[this.activeLayer].getNumObservations());
      } //end if layerType
    } //next layer

    this.fitOnce = true;
  }


  //start component event handling
  //note: this class only listens to itself
  /***
   * Interface methods for component listener.
   */
  public void componentHidden(ComponentEvent e) {
  }

  public void componentShown(ComponentEvent e) {
    this.componentResized(e);
    System.out.println("showing component");
  }

  public void componentMoved(ComponentEvent e) {
  }

  public void componentResized(ComponentEvent e) {
    if ( (this.shapeLayers.size() > 0) && (this.getWidth() > 0) &&
        (this.getHeight() > 0)) {
      this.drawingBuff = this.createImage(this.getWidth(), this.getHeight());

      for (int i = 0; i < this.shapeLayers.size(); i++) {
        LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(i);
        ls.setParentSize(this.getHeight(), this.getWidth());
      } //next layer

      if (this.autofit || !this.fitOnce) {
        this.autoFitShapes();
      } //end if autofit

      if (this.fisheyes != null) {
        float width = (float)this.getWidth();
        this.fisheyes.setLensRadius(width / 5f);
      }
      //hack
      //this.sendColorsToLayers(this.dataColorX.length);
      LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
      ls.setObjectColors(this.objectColors);
      //previous line results in sending colors to layers
      paintDrawingBuff();
    } //end if layers exist and size is > 0
    //System.out.println("resizing component");
  }

  //end component event handling
  //start accessors
  public void setMode(int mode) {
    if (mode == this.MODE_SELECT) {
      this.fisheyes = null;
      this.exLabels = null;
      this.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
    }
    else if (mode == this.MODE_PAN) {
      this.setCursor(cursors.getCursor(GeoCursors.CURSOR_PAN));
    }
    else if (mode == this.MODE_ZOOM_IN) {
      this.setCursor(cursors.getCursor(GeoCursors.CURSOR_ZOOM_IN));
    }
    else if (mode == this.MODE_ZOOM_OUT) {
      this.setCursor(cursors.getCursor(GeoCursors.CURSOR_ZOOM_OUT));
    }
    else if (mode == this.MODE_EXCENTRIC) {
      this.exLabels = new ExcentricLabels();
      this.initExcentricLabels();
      //this.exLabels.setVisible(true);
      this.fisheyes = null;
      this.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
    }
    else if (mode == this.MODE_FISHEYE) {
      this.exLabels = null;
      this.fisheyes = new Fisheyes();
      fisheyes.setLensType(Fisheyes.LENS_GAUSSIAN);
      this.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
    }

    else {
      throw new IllegalArgumentException(
          "MapCanvas.setMode, ecountered unknown mode");
    }

    this.mode = mode;
  }

  public void setBivarColorClasser(BivariateColorSymbolClassification
                                   bivarColorClasser) {
    this.bivarColorClasser = (BivariateColorSymbolClassificationSimple)
        bivarColorClasser;

    if (this.dataSets != null && this.dataSets[this.activeLayer] != null) {
      this.sendColorsToLayers(this.dataSets[this.activeLayer].getNumObservations());
    }
  }

  public void setAuxiliarySpatialData(Object[] dataSetIn) {
    if (dataSetIn == null) {
      return;
    }
    String[] attributeArrays = (String[]) dataSetIn[0];
    int len = attributeArrays.length;
    DataSetForApps auxData = new DataSetForApps();
    auxData.setDataObject(dataSetIn);

    if (dataSetIn[len + 1] == null) {
      auxData.setObservationNames(null);
    }
    else if (dataSetIn[len + 1] instanceof String[]) {
      auxData.setObservationNames( (String[]) dataSetIn[len + 1]);
    }

    this.setAuxiliarySpatialData(auxData);
  }

  /***
   * Add more layers, in addition to the active layer.
   */
  public void setAuxiliarySpatialData(DataSetForApps auxData) {
    if (auxData == null) {
      return;
    }
    LayerShapeExtension ls = null;
    int layerType = auxData.getSpatialType();

    if (layerType == DataSetForApps.SPATIAL_TYPE_POLYGON) {
      ls = new LayerPolygonExtension();

      ls.setOriginalSpatialData(auxData.getShapeData());
      spatialDataTypes.add(new Integer(DataSetForApps.SPATIAL_TYPE_POLYGON));
      Shape[] userShapes = auxData.getShapeData();

      userShapes = this.findFullExtentSpatialData(userShapes, true);
      this.sendSpatialDataToLayer(ls, userShapes);
    }
    else if (layerType == DataSetForApps.SPATIAL_TYPE_POINT) {
      LayerPointExtension lp = new LayerPointExtension();
      Shape[] pointShapesOriginal = lp.pointsToShapes(auxData.getPoint2DData());
      Shape[] pointShapesUser = this.findFullExtentSpatialData(
          pointShapesOriginal, true);
      Point2D[] userSpacePoints = lp.shapesToPoints(pointShapesUser);
      //Transform points to cirles so that they can be filled.
      Shape[] circleShapes = lp.findShapesForPoints(userSpacePoints);
      lp.setOriginalSpatialData(pointShapesOriginal);
      spatialDataTypes.add(new Integer(DataSetForApps.SPATIAL_TYPE_POINT));
      //userSpaceSpatialData.add(circleShapes);
      lp.setSpatialData(circleShapes);
      ls = lp;
    }
    else {
      throw new IllegalArgumentException(
          "unexpected layer type ecountered in MapCanvas.setAuxiliarySpatialData");
    }

    //ok, default behaviours here:
    //new layer goes on top of previous layer
    //so add it to the end
    //new layer has no fill
    ls.setIsAuxiliary(true);
    this.shapeLayers.add(ls);
    ls.setParentSize(this.getHeight(), this.getWidth());
    this.paintDrawingBuff();
    this.repaint();
  }

  public void setDataSet(Object[] dataSetIn) {
    int numLayers = this.shapeLayers.size();
    if (this.dataSets == null)
      this.dataSets = new DataSetForApps[10];

    if (numLayers == this.dataSets.length)
    {
      DataSetForApps[] temp = new DataSetForApps[numLayers*2];
      System.arraycopy(this.dataSets, 0, temp, 0, numLayers);
      this.dataSets = temp;
    }

    String[] attributeArrays = (String[]) dataSetIn[0];
    int len = attributeArrays.length;
    this.dataSets[numLayers] = new DataSetForApps();
    this.dataSets[numLayers].setDataObject(dataSetIn);

    if (dataSetIn[len + 1] == null) {
      this.dataSets[numLayers].setObservationNames(null);
    }
    else if (dataSetIn[len + 1] instanceof String[]) {
      this.dataSets[numLayers].setObservationNames( (String[]) dataSetIn[len + 1]);
    }

    this.setDataSet(numLayers, this.dataSets[numLayers]);
    this.initExcentricLabels();
  }

  private void setDataSet(int idx, DataSetForApps dataSet) {
    this.dataColorX = null;
    this.dataColorY = null;
    this.currColorColumnX = -1;
    this.currColorColumnY = -1;
    boolean isAuxiliaryData = (this.activeLayer != idx);

    String[] variableNames = dataSet.getAttributeNamesNumeric();

    int layerType = dataSet.getSpatialType();
    LayerShapeExtension ls = null;
    String[] attNames = dataSet.getAttributeNamesNumeric();
    int numAttributes = attNames.length;

    if (layerType == DataSetForApps.SPATIAL_TYPE_POLYGON) {
      System.out.println("Add polygon data, activelayer? " + (!isAuxiliaryData));
      ls = new LayerPolygonExtension();
      ls.setOriginalSpatialData(dataSet.getShapeData());

      spatialDataTypes.add(new Integer(DataSetForApps.SPATIAL_TYPE_POLYGON));

      Shape[] userShapes = dataSet.getShapeData();
      userShapes = this.findFullExtentSpatialData(userShapes, isAuxiliaryData);

      //userSpaceSpatialData.add(userShapes);
      this.sendSpatialDataToLayer(ls, userShapes);
    }
    else if (layerType == DataSetForApps.SPATIAL_TYPE_POINT) {
      System.out.println("Add point data, activelayer? " + (!isAuxiliaryData));
      LayerPointExtension lp = new LayerPointExtension();
      Shape[] pointShapesOriginal = lp.pointsToShapes(dataSet.getPoint2DData());
      Shape[] pointShapesUser = this.findFullExtentSpatialData(
          pointShapesOriginal, isAuxiliaryData);

      Point2D[] userSpacePoints = lp.shapesToPoints(pointShapesUser);

      //Transform points to cirles so that they can be filled.
      Shape[] circleShapes = lp.findShapesForPoints(userSpacePoints);
      lp.setOriginalSpatialData(pointShapesOriginal);
      spatialDataTypes.add(new Integer(DataSetForApps.SPATIAL_TYPE_POINT));

      //userSpaceSpatialData.add(circleShapes);
      lp.setSpatialData(circleShapes);
      ls = lp;
    }
    else if (layerType == DataSetForApps.SPATIAL_TYPE_LINE) {
      System.out.println("Add line data, activelayer? " + (!isAuxiliaryData));
      LayerLineExtension ll = new LayerLineExtension();

      Shape[] temp = dataSet.getShapeData();
      Shape[] tt = new Shape[temp.length - 1];
      System.arraycopy(temp, 0, tt, 0, tt.length);

      ll.setOriginalSpatialData(tt);
      Shape[] userShapes = tt;
      spatialDataTypes.add(new Integer(DataSetForApps.SPATIAL_TYPE_LINE));
      userShapes = this.findFullExtentSpatialData(userShapes, isAuxiliaryData);
      this.sendSpatialDataToLayer(ll, userShapes);
      ls = ll;
    }

    ls.setIsAuxiliary(isAuxiliaryData);
    shapeLayers.add(ls);

    //set default data to get color from
    if ( (this.drawingBuff == null) && (this.getWidth() > 0) &&
        (this.getHeight() > 0)) {
      this.drawingBuff = this.createImage(this.getWidth(), this.getHeight());
    }

    if (this.drawingBuff != null) {
    }

    //resizing will autofit if this.autofit = true
    //resizing resizes the drawing buffer, and repaints it
    boolean realAutofit = this.autofit;
    this.autofit = true;
    this.componentResized(new ComponentEvent(this, 0));
    this.autofit = realAutofit;
  }

  public void setTransformer(ShapeTransformer transformer) {
    this.transformer = transformer;
  }

  public void setCurrColorColumnX(int currColorColumnX) {
    if ( (this.dataSets[this.activeLayer] != null) &&
        (currColorColumnX <= this.dataSets[this.activeLayer].getNumberNumericAttributes())) {
      this.currColorColumnX = currColorColumnX;
      //XXX getNumericDataAsDouble has changed...
      this.dataColorX = this.dataSets[this.activeLayer].getNumericDataAsDouble(currColorColumnX -
          1);
      this.sendColorsToLayers(this.dataColorX.length);
    }
  }

  public int getCurrColorColumnX() {
    return this.currColorColumnX;
  }

  public Color[] getColors() {
    if (this.shapeLayers.size() == 0) {
      return new Color[0];
    }

    LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);

    return ls.getColors();
  }

  public void setCurrColorColumnY(int currColorColumnY) {
    if ( (this.dataSets!= null && this.dataSets[this.activeLayer] != null) &&
        (currColorColumnY <= this.dataSets[this.activeLayer].getNumberNumericAttributes())) {
      this.currColorColumnY = currColorColumnY;
      //XXX getNumericDataAsDouble has changed...
      this.dataColorY = this.dataSets[this.activeLayer].getNumericDataAsDouble(currColorColumnY -
          1);
      this.sendColorsToLayers(this.dataColorY.length);
    }
  }

  public int getCurrColorColumnY() {
    return this.currColorColumnY;
  }

  public void setIndication(int indication) {
    if (fisheyes != null) {
      //if we have fisheyes, this is too expensive 
      //bail
      return;
    }

    if (indication != this.indication) {
      this.indication = indication;

      if (shapeLayers.size() > 0) {
        LayerShapeExtension ls = (LayerShapeExtension) shapeLayers.elementAt(this.activeLayer);
        ls.setIndication(indication);

        if (indication >= 0 && indication < this.dataSets[this.activeLayer].getNumObservations()) {
          this.drawTip = true;

          Point p = ls.findCentroid(indication);
          this.mouseX2 = (int) p.getX();
          this.mouseY2 = (int) p.getY();
        }
        else {
          this.drawTip = false;
        }

        this.makeToolTip(indication);
      } //if we have at least one layer

      //paintDrawingBuff();
      this.repaint();
    } //if indication is new
  }

  public void setSelectedObservations(Vector selectedObservations) {
    this.selectedObservations = new int[selectedObservations.size()];

    int i = 0;

    for (Enumeration e = selectedObservations.elements(); e.hasMoreElements(); ) {
      Integer bigIint = (Integer) e.nextElement();
      this.selectedObservations[i] = bigIint.intValue();
      i++;
    }

    this.setSelectedObservationsInt(this.selectedObservations);
  }

  public Vector getSelectedObservations() {
    Vector v = new Vector();

    for (int i = 0; i < selectedObservations.length; i++) {
      v.add(new Integer(selectedObservations[i]));
    }

    return v;
  }

  public void setSelections(int[] selections) {
    Vector v = new Vector();

    for (int i = 0; i < selections.length; i++) {
      if (selections[i] == 1) {
        v.add(new Integer(i));
      }
    }

    this.selectedObservations = new int[v.size()];

    int i = 0;

    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      Integer bigIint = (Integer) e.nextElement();
      this.selectedObservations[i] = bigIint.intValue();
      i++;
    }

    this.setSelectedObservationsInt(this.selectedObservations);
  }

  public int[] getSelections() {
    int[] selections = new int[this.dataColorX.length];
    if (this.selectedObservations == null) {
      this.selectedObservations = new int[0];
    }
    for (int i = 0; i < this.selectedObservations.length; i++) {
      selections[this.selectedObservations[i]] = 1;
    }

    return selections;
  }

  public void selectionChanged(SelectionEvent e) {
    int[] sel = e.getSelection();
    this.setSelectedObservationsInt(sel);
  }

  public void indicationChanged(IndicationEvent e) {
    if (this == e.getSource()) {
      return;
    }

    int indication = e.getIndication();
    this.setIndication(indication);
  }

  public void dataSetChanged(DataSetEvent e) {
      this.setDataSet(e.getDataSet());

//      System.out.println("set auxiliary data layer");
//      this.setAuxiliarySpatialData(e.getDataSet());
  }

  public void setSelectedObservationsInt(int[] selectedObservations) {
    this.selectedObservations = selectedObservations;

    if (selectedObservations == null) {
      return;
    }

    if ( (shapeLayers.size() > 0) && (this.drawingBuff != null)) {
      LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
      ls.setSelectedObservations(this.selectedObservations);
      paintDrawingBuff();
      this.repaint();
    }
    else if ( (shapeLayers.size() > 0) && (this.drawingBuff == null) &&
             (this.selectedObservations.length > 1)) { //means we have data but are not visible yet

      LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
      ls.setSelectedObservations(this.selectedObservations);
    }
  }

  public int[] getSelectedObservationsInt() {
    return this.selectedObservations;
  }

  public void setColorSelection(Color colorSelection) {
    this.colorSelection = colorSelection;

    if (shapeLayers.size() > 0) {
      for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
        LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
        ls.setColorSelection(colorSelection);
      }

      paintDrawingBuff();
      this.repaint();
    }
  }

  public boolean getSelOriginalColorMode() {
    return selOriginalColorMode;
  }

  public void setSelOriginalColorMode(boolean selOriginalColorMode) {
    this.selOriginalColorMode = selOriginalColorMode;
    if (shapeLayers == null) {
      return;
    }
    for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
      LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
      ls.setSelOriginalColorMode(selOriginalColorMode);
    }

    paintDrawingBuff();
    this.repaint();

  }

  public void setBackground(Color c) {
    super.setBackground(c);
    if (shapeLayers == null) {
      return;
    }
    for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
      LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
      ls.setColorBackground(c);
    }

    paintDrawingBuff();
    this.repaint();

  }


  //set clustering color
  public void setClusteringColor(Color[] clusteringColor){
    LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    ls.setObjectColors(clusteringColor);
    paintDrawingBuff();
    this.repaint();
  }

  /**
   * put your documentation comment here
   * @param conditionArray
   */
  public void setConditionArray(int[] conditionArray) {
    if (shapeLayers.size() > 0 && drawingBuff != null) {
      for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
        LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
        ls.setConditionArray(conditionArray);
      }

      paintDrawingBuff();
      this.repaint();
    }
  }

  //end accessors
  public void actionPerformed(ActionEvent e) {
    //* this method used for testing only...
     String source = e.getSource().toString();
    String command = e.getActionCommand();

    if (e.getSource()instanceof ConditionManager) {
      ConditionManager cm = (ConditionManager) e.getSource();
      int[] conditionArray = cm.getConditionResults();
      this.setConditionArray(conditionArray);
    }

    //*/
  }


  /**
   * Draws a bounding box for selection.
   * @param e
   */
  public void mouseDragged(MouseEvent e) {
    this.drawBox = true;
    this.drawTip = false;

    if (mode == this.MODE_PAN) {
      Cursor grabCur = cursors.getCursor(GeoCursors.CURSOR_GRAB);

      if (this.getCursor() != grabCur) {
        this.setCursor(grabCur);
      }
    }

    mouseX2 = e.getX();
    mouseY2 = e.getY();

    if (mode == this.MODE_PAN) {
      panBuff(mouseX1, mouseX2, mouseY1, mouseY2);
    }

    repaint();
  }

  /**
   * Activates a tool tip.
   * @param e
   */
  public void mouseMoved(MouseEvent e) {
    if (this.shapeLayers.size() < 1) {
      return;
    }

    if (fisheyes != null) {
      fisheyes.setFocus(e.getX(), e.getY());
      repaint();
    }

    LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    int indic = ls.findIndication(e.getX(), e.getY());

    if (indic != indication) {
      this.setIndication(indic);

      int xClass = -1;
      int yClass = -1;

      if (indic > 0) {
        xClass = this.bivarColorClasser.getClassX(indic);
        yClass = this.bivarColorClasser.getClassY(indic);
      }

      this.fireIndicationChanged(indic, xClass, yClass);
    }

    mouseX2 = e.getX();
    mouseY2 = e.getY();
  }

  /**
   * Inits selection bounding box.
   * @param e
   */
  public void mousePressed(MouseEvent e) {
    mouseX1 = e.getX();
    mouseY1 = e.getY();
    mouseX2 = e.getX();
    mouseY2 = e.getY();
  }

  /**
   * Makes selection.
   * @param e
   */
  public void mouseReleased(MouseEvent e) {
    if (this.dataSets == null || this.dataSets[this.activeLayer] == null) {
      return;
    }

    if (mode == this.MODE_PAN) {
      this.setCursor(cursors.getCursor(GeoCursors.CURSOR_PAN));
    }

    panningXForm.setToIdentity();

    mouseX2 = e.getX();
    mouseY2 = e.getY();

    if (mode == this.MODE_SELECT) {
      if (mouseX1 > mouseX2) {
        int temp = mouseX1;
        mouseX1 = mouseX2;
        mouseX2 = temp;
      }

      if (mouseY1 > mouseY2) {
        int temp = mouseY1;
        mouseY1 = mouseY2;
        mouseY2 = temp;
      }

      if (e.isShiftDown()) {
        this.makeSelectionShift(mouseX1, mouseX2 + 1, mouseY1, mouseY2 + 1);
      }
      else {
        this.makeSelection(mouseX1, mouseX2 + 1, mouseY1, mouseY2 + 1);
      }
    }
    else if (mode == this.MODE_ZOOM_IN) {
      this.zoomIn(mouseX1, mouseX2, mouseY1, mouseY2);
    }
    else if (mode == this.MODE_ZOOM_OUT) {
      this.zoomOut(mouseX1, mouseX2, mouseY1, mouseY2);
    }
    else if (mode == this.MODE_PAN) {
      this.pan(mouseX1, mouseX2, mouseY1, mouseY2);
    }

    this.drawBox = false;
    repaint();
  }

  /**
   * makes crosshair cursor
   * @param e
   */
  public void mouseEntered(MouseEvent e) {
  }

  /**
   * resets cursor
   * @param e
   */
  public void mouseExited(MouseEvent e) {
    e.consume();
    this.drawTip = false; //prevents toolip from drawing
    this.drawBox = false;
    this.repaint();
    this.setIndication( -1);
    this.fireIndicationChanged( -1, 0, 0);

    if (fisheyes != null) {
      fisheyes.setFocus( -1000f, -1000f);
    }
  }

  /**
   * pop up a detail map
   * @param e
   */
  public void mouseClicked(MouseEvent e) {
    if ( (e.getSource() == this) && (e.getClickCount() > 1)) { // This is a double-click or triple...

      //if (dataIndices[0] != dataIndices[1]) { //why this??? I guess we don't want to pop up one from the
      //diagonal if we are a scatterplot
      GeoMap detailMap = new GeoMap();
      detailMap.setBackground(this.getBackground());
      detailMap.setDataSet(this.dataSets[this.activeLayer].getDataObjectOriginal());
      detailMap.setBivarColorClasser(this.bivarColorClasser);
      detailMap.setSelectedObservations(this.getSelectedObservationsInt()); //need to do this here because

      //otherwise the selection won't "take"
      detailMap.setXVariable(currColorColumnX);
      detailMap.setXChooserMode(GeoMap.VARIABLE_CHOOSER_MODE_FIXED);
      detailMap.setYVariable(currColorColumnY);
      detailMap.setYChooserMode(GeoMap.VARIABLE_CHOOSER_MODE_FIXED);

      JFrame dummyFrame = new JFrame();
      JDialog detailMapFrame = new JDialog(dummyFrame, "Detail Map", true);

      detailMapFrame.setLocation(300, 300);
      detailMapFrame.setSize(300, 300);
      detailMapFrame.getContentPane().setLayout(new BorderLayout());
      detailMapFrame.getContentPane().add(detailMap, BorderLayout.CENTER);

      CoordinationManager cm = new CoordinationManager();
      cm.addBean(this);
      cm.addBean(detailMap);

      detailMapFrame.show();

      //}//end dataIndeces
    } //end if doubleclick
  } //end method

  private void makeSelection(int x1, int x2, int y1, int y2) {
    int xDiff = Math.abs(x2 - x1);
    int yDiff = Math.abs(y2 - y1);
    int minPixels = 3;

    if ( (xDiff < minPixels) && (yDiff < minPixels)) {
      return;
    }

    if (shapeLayers.size() > 0) {
      LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
      ls.findSelection(x1, x2, y1, y2);
      this.selectedObservations = ls.getSelectedObservations();

      //let's try just redrawing the selection
      //for (int i = 0; i < selectedObservations.length; i++) {
      ls.setSelectedObservations(selectedObservations);

      this.fireActionPerformed(LayerShapeExtension.COMMAND_SELECTION);
      this.fireSelectionChanged(this.selectedObservations);

      //unfortunately, we need to completely redraw any aux layers
      this.paintAuxLayers();

      //XXX shouldn't have to do this
      paintDrawingBuff();
      this.repaint();
    }
  } //method

  private void makeSelectionShift(int x1, int x2, int y1, int y2) {
    if (shapeLayers.size() > 0) {
      for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
        LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
        ls.findSelectionShift(x1, x2, y1, y2);
        this.selectedObservations = ls.getSelectedObservations();
      }

      this.fireActionPerformed(LayerShapeExtension.COMMAND_SELECTION);
      paintDrawingBuff();
      this.repaint();
    }
  }

  //end mouse event handling

  /**
   * Attention all layers! paint yourselves onto the buffer.
   * @param g
   */
  private void paintDrawingBuff() {
    if (this.drawingBuff == null) {
      return;
    }

    Graphics g = this.drawingBuff.getGraphics();
    Graphics2D g2 = (Graphics2D) g;
    g.setColor(this.getBackground());
    g.fillRect(0, 0, this.getWidth(), this.getHeight());

    if (shapeLayers.size() > 0) {
      for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
        LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();
        ls.fisheyes = this.fisheyes;
        ((LayerInterface)ls).render(g2); //paint your whole self
      } //next element
    } //end if
  }

  /**
   * Attention all auxiliary layers! paint yourselves onto the buffer.
   * @param g
   */
  private void paintAuxLayers() {
    if (this.drawingBuff == null) {
      return;
    }

    Graphics g = this.drawingBuff.getGraphics();
    Graphics2D g2 = (Graphics2D) g;

    if (shapeLayers.size() > 0) {
      for (Enumeration e = shapeLayers.elements(); e.hasMoreElements(); ) {
        LayerShapeExtension ls = (LayerShapeExtension) e.nextElement();

        if (ls.getIsAuxiliary()) { //if you are aux
          ls.fisheyes = this.fisheyes;
          ((LayerInterface)ls).render(g2); //paint your whole self
        } //end if aux
      } //next element
    } //end if
  }

  /**
   * paints buffer, then drawing box
   * @param g
   */
  public void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2 = (Graphics2D) g;

    //Fish eye
    if ( (this.drawingBuff == null) || (this.fisheyes != null)) {
      this.drawingBuff = this.createImage(this.getWidth(), this.getHeight());
      paintDrawingBuff();
    }

    //Draw buff
    if (this.drawingBuff != null) {
      g2.drawImage(this.drawingBuff, this.panningXForm, this);
      //g.drawImage(this.drawingBuff,0,0,this);
    }
    //Just draw background
    else {
      g.fillRect(0, 0, this.getWidth(), this.getHeight());
    }

    //Draw indication.
    if ( (this.indication != Integer.MIN_VALUE) && (shapeLayers.size() > 0) &&
        (this.mode != this.MODE_PAN) && (this.fisheyes == null)) {
      LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);

      if (indication >= 0){
        //This is the place to draw layer.
          ((LayerInterface)ls).renderObservation(indication, g2);
      }

      if (this.symbolset){
        for (int path = 0; path < ls.spatialData.length; path++) {
          ((LayerInterface)ls).renderObservationNoIndication(path, g2);
        }
        this.symbolset = false;
      }
    }

    drawSelectionBox(g2);

    //this.toolTip.paint(g);
    if (this.drawTip && (exLabels != null) &&
        (this.exLabels.isVisible() == false)) {
      drawTooltip(g2, this.tipText, this.mouseX2, this.mouseY2, Color.lightGray);
    }

    if (exLabels != null) {
      exLabels.paint(g2, getBounds());
    }
  }

  /**
   * Helper function to draw a "tooltip" on the given graphics object.
   *
   * @param g2 The Graphics2D Object to draw on.
   * @param text The (multiline) text of the tooltip.
   * @param x The x coordinate.
   * @param y The y coordinate.
   * @param col The background color.
   */
  private void drawTooltip(Graphics2D g2, String text, int x, int y, Color col) {
    if (!drawTip) {
      return;
    }

    int i;
    int mheight;
    int mwidth = 0;
    int numLines;
    int lineHeight;
    Font f = g2.getFont();
    tipFont = f.deriveFont(9f);
    g2.setFont(tipFont);

    StringTokenizer tok = new StringTokenizer(text, "\n");
    numLines = tok.countTokens();

    String[] lines = new String[numLines];

    for (i = 0; i < numLines; i++) {
      lines[i] = tok.nextToken();

      int tempwidth = g2.getFontMetrics().stringWidth(lines[i]) + 6;

      if (tempwidth > mwidth) {
        mwidth = tempwidth;
      }
    }

    lineHeight = g2.getFontMetrics().getHeight();
    mheight = (numLines * lineHeight) + 2;

    x += 10;
    y += 10;

    //if we are too close to the right side....
    if ( (x + mwidth) > this.getWidth()) {
      x -= (mwidth + 20);
    }

    //if we are too close to the bottom...
    if ( (y + mheight) > this.getHeight()) {
      y -= (mheight + 20);
    }

    AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
        0.7f);
    g2.setComposite(ac);

    g2.setStroke(new BasicStroke(0.5f));
    g2.setColor(new Color(0.2f, 0.2f, 0.2f, 0.7f));

    //g2.drawRect(x, y, mwidth, mheight);
    g2.setColor(col);
    g2.fillRect(x + 1, y + 1, mwidth - 1, mheight - 1);

    g2.setColor(Color.black);

    ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER);
    g2.setComposite(ac);

    for (i = 0; i < numLines; i++) {
      g2.drawString(lines[i], x + 3, (y + ( (i + 1) * lineHeight)) - 4);
    }
  }

  /****
   *
   */
  private void drawSelectionBox(Graphics2D g2) {
    if (!drawBox) {
      return;
    }

    Stroke tempStroke = g2.getStroke();
    Stroke boxStroke = null;

    if (this.mode == this.MODE_SELECT) {
      boxStroke = this.dashStroke;
    }
    else if ( (this.mode == this.MODE_ZOOM_IN) ||
             (this.mode == this.MODE_ZOOM_OUT)) {
      boxStroke = this.solidStroke;
    }
    else {
      return;
    }

    g2.setStroke(boxStroke);
    g2.setPaintMode();
    g2.setColor(Color.black);
    g2.setXORMode(Color.white);

    //let's take drawing the selection rectangle by cases
    //not elegant, but the alternative is introducing more class variables
    int selectX = 0;
    int selectY = 0;
    int selectWidth = 0;
    int selectHeight = 0;

    if ( (mouseX1 <= mouseX2) && (mouseY1 <= mouseY2)) {
      selectX = mouseX1;
      selectY = mouseY1;
      selectWidth = mouseX2 - mouseX1;
      selectHeight = mouseY2 - mouseY1;
    }

    if ( (mouseX2 < mouseX1) && (mouseY1 <= mouseY2)) {
      selectX = mouseX2;
      selectY = mouseY1;
      selectWidth = mouseX1 - mouseX2;
      selectHeight = mouseY2 - mouseY1;
    }

    if ( (mouseX1 <= mouseX2) && (mouseY2 < mouseY1)) {
      selectX = mouseX1;
      selectY = mouseY2;
      selectWidth = mouseX2 - mouseX1;
      selectHeight = mouseY1 - mouseY2;
    }

    if ( (mouseX2 < mouseX1) && (mouseY2 < mouseY1)) {
      selectX = mouseX2;
      selectY = mouseY2;
      selectWidth = mouseX1 - mouseX2;
      selectHeight = mouseY1 - mouseY2;
    }

    g2.drawRect(selectX, selectY, selectWidth, selectHeight);
    g2.setStroke(tempStroke);
  }

  public int getNumberOfLayers()
  {
    return this.shapeLayers.size();
  }

  //start excentric labeling stuff
  private void initExcentricLabels() {
    if (exLabels != null) {
      this.exLabels = new ExcentricLabels();
      exLabels.setComponent(this);
      exLabels.setOpaque(true);

      Color halfWhite = new Color(255, 255, 255, 123);
      exLabels.setBackgroundColor(halfWhite);
      this.addMouseListener(exLabels);
    }
  }

  public String getObservationLabel(int i) {
    String[] labels = this.dataSets[this.activeLayer].getObservationNames();
    String label = labels[i];

    return label;
  }

  public Shape getShapeAt(int i) {
    LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    Shape shp = ls.getSpatialData()[i];

    return shp;
  }

  public int[] pickAll(Rectangle2D hitBox) {
    LayerShapeExtension ls = (LayerShapeExtension)this.shapeLayers.elementAt(this.activeLayer);
    int[] selObs = ls.findSelection(hitBox);

    return selObs;
  }

  //end excentric labeling stuff

  /**
   * implements ActionListener
   */
  public void addActionListener(ActionListener l) {
    listenerList.add(ActionListener.class, l);
  }

  /**
   * removes an ActionListener from the button
   */
  public void removeActionListener(ActionListener l) {
    listenerList.remove(ActionListener.class, l);
  }

  /**
   * Notify all listeners that have registered interest for
   * notification on this event type. The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   * @see EventListenerList
   */
  protected void fireActionPerformed(String command) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    ActionEvent e = null;

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == ActionListener.class) {
        // Lazily create the event:
        if (e == null) {
          e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command);
        }

        ( (ActionListener) listeners[i + 1]).actionPerformed(e);
      }
    }
  }

  /**
   * adds an IndicationListener
   */
  public void addIndicationListener(IndicationListener l) {
    listenerList.add(IndicationListener.class, l);
  }

  /**
   * removes an IndicationListener from the component
   */
  public void removeIndicationListener(IndicationListener l) {
    listenerList.remove(IndicationListener.class, l);
  }

  /**
   * Notify all listeners that have registered interest for
   * notification on this event type. The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   * @see EventListenerList
   */
  private void fireIndicationChanged(int newIndication, int xClass, int yClass) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    IndicationEvent e = null;

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == IndicationListener.class) {
        // Lazily create the event:
        if (e == null) {
          e = new IndicationEvent(this, newIndication, xClass, yClass);
        }

        ( (IndicationListener) listeners[i + 1]).indicationChanged(e);
      }
    } //next i
  }

  /**
   * adds an SelectionListener
   */
  public void addSelectionListener(SelectionListener l) {
    //System.out.println("mapCan, selection listeners = " + listenerList.getListenerCount(SelectionListener.class));
    listenerList.add(SelectionListener.class, l);
  }

  /**
   * removes an SelectionListener from the component
   */
  public void removeSelectionListener(SelectionListener l) {
    //System.out.println("mapCan, removing a selection listener");
    listenerList.remove(SelectionListener.class, l);
  }

  /**
   * Notify all listeners that have registered interest for
   * notification on this event type. The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   * @see EventListenerList
   */
  public void fireSelectionChanged(int[] newSelection) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    SelectionEvent e = null;

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == SelectionListener.class) {
        // Lazily create the event:
        if (e == null) {
          e = new SelectionEvent(this, newSelection);
        }

        ( (SelectionListener) listeners[i + 1]).selectionChanged(e);
      }
    } //next i
  }

  /**
   * adds an SpatialExtentListener
   */
  public void addSpatialExtentListener(SpatialExtentListener l) {
    listenerList.add(SpatialExtentListener.class, l);
  }

  /**
   * removes an SpatialExtentListener from the component
   */
  public void removeSpatialExtentListener(SpatialExtentListener l) {
    listenerList.remove(SpatialExtentListener.class, l);
  }

  /**
   * Notify all listeners that have registered interest for
   * notification on this event type. The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   * @see EventListenerList
   */
  private void fireSpatialExtentChanged(Rectangle2D newSpatialExtent) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    SpatialExtentEvent e = null;

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == SpatialExtentListener.class) {
        // Lazily create the event:
        if (e == null) {
          e = new SpatialExtentEvent(this, newSpatialExtent);
        }

        ( (SpatialExtentListener) listeners[i + 1]).spatialExtentChanged(e);
      }
    } //next i
  }

  public void setActiveLayerIdx(int idx){
    int numlayers = this.getNumberOfLayers();
    if (numlayers > 0 && idx < numlayers){
      this.activeLayer = idx;
      ((LayerShapeExtension) this.shapeLayers.elementAt(this.activeLayer)).setIsAuxiliary(false);
      for (int i=0; i< numlayers; i++){
        if (i!= this.activeLayer)
        ((LayerShapeExtension) this.shapeLayers.elementAt(i)).setIsAuxiliary(true);
      }
      this.validate();
      this.repaint();
    }
  }

  public DataSetForApps getActiveLayerData(){
    return this.dataSets[this.activeLayer];
  }
}